---
title: "Setup"
description: "Follow this guide to set up Novel with Tailwindcss"
---

<Info>
  This example demonstrates the use of Shadcn-ui for ui, but alternative
  libraries and components can also be employed.
</Info>

<Card
  title="Shadcn-ui"
  icon="link"
  href="https://ui.shadcn.com/docs/installation"
>
  You can find more info about installing shadcn-ui here. You will need to add
  the following components: <b>Button, Separator, Popover, Command, Dialog</b>
</Card>

This example will use the same stucture from here: [Anatomy](/quickstart#anatomy)\
You can find the full example here: [Tailwind Example](https://github.com/steven-tey/novel/blob/main/apps/web/components/tailwind/editor.tsx)

## Configure Wrapper

    ```tsx
    "use client";

    import { EditorContent, EditorRoot } from "novel";
    import { useState } from "react";

    const TailwindEditor = () => {
      const [content, setContent] = useState(null);
      return (
        <EditorRoot>
          <EditorContent
            initialContent={content}
            onUpdate={({ editor }) => {
              const json = editor.getJSON();
              setContent(json);
            }}
          />
        </EditorRoot>
      );
    };
    export default TailwindEditor;
    ```
    <Tip>
      `onUpdate` runs on every change. In most cases, you will want to debounce the updates to prevent too many state changes.
      ```tsx
      import { EditorInstance } from "novel"

      ...
      const debouncedUpdates = useDebouncedCallback(async (editor: EditorInstance) => {
        const json = editor.getJSON();
        setContent(json);
        setSaveStatus("Saved");
      }, 500);

      onUpdate={debouncedUpdates};
      ```
    </Tip>

## Configure Extensions

    <Card title='Extensions' icon='link' href='/guides/tailwind/extensions'>
      You can find here example of extensions
    </Card>

    ```tsx
    import { defaultExtensions } from "./extensions";

    const extensions = [...defaultExtensions];

    <EditorContent
      extensions={extensions}
      ...
    />;
    ```

## Create Menus

<CardGroup cols={2}>
  <Card
    title="Slash Command"
    href="/guides/tailwind/slash-command"
    icon="terminal"
  >
    Slash commands are a way to quickly insert content into the editor.
  </Card>
  <Card
    title="Bubble Menu"
    href="/guides/tailwind/bubble-menu"
    icon="square-caret-down"
  >
    The bubble menu is a context menu that appears when you select text.
  </Card>
</CardGroup>

## Add Editor Props

`handleCommandNavigation` is required for fixing the arrow navigation in the / command;

```tsx
import { handleCommandNavigation } from "novel/extensions";
import { defaultEditorProps, EditorContent } from "novel";

<EditorContent
  ...
  editorProps={{
      handleDOMEvents: {
        keydown: (_view, event) => handleCommandNavigation(event),
      },
      attributes: {
        class: `prose prose-lg dark:prose-invert prose-headings:font-title font-default focus:outline-none max-w-full`,
      }
  }}
/>
```

## Add Styles

<AccordionGroup>
  <Accordion title='Prosemirror Styles' icon='css3'>
    ```css prosemirror.css
    .ProseMirror {
      @apply p-12 px-8 sm:px-12;
    }

    .ProseMirror .is-editor-empty:first-child::before {
      content: attr(data-placeholder);
      float: left;
      color: hsl(var(--muted-foreground));
      pointer-events: none;
      height: 0;
    }
    .ProseMirror .is-empty::before {
      content: attr(data-placeholder);
      float: left;
      color: hsl(var(--muted-foreground));
      pointer-events: none;
      height: 0;
    }

    /* Custom image styles */
    .ProseMirror img {
      transition: filter 0.1s ease-in-out;

      &:hover {
        cursor: pointer;
        filter: brightness(90%);
      }

      &.ProseMirror-selectednode {
        outline: 3px solid #5abbf7;
        filter: brightness(90%);
      }
    }

    .img-placeholder {
      position: relative;

      &:before {
        content: "";
        box-sizing: border-box;
        position: absolute;
        top: 50%;
        left: 50%;
        width: 36px;
        height: 36px;
        border-radius: 50%;
        border: 3px solid var(--novel-stone-200);
        border-top-color: var(--novel-stone-800);
        animation: spinning 0.6s linear infinite;
      }
    }

    @keyframes spinning {
      to {
        transform: rotate(360deg);
      }
    }

    /* Custom TODO list checkboxes – shoutout to this awesome tutorial: https://moderncss.dev/pure-css-custom-checkbox-style/ */
    ul[data-type="taskList"] li > label {
      margin-right: 0.2rem;
      user-select: none;
    }

    @media screen and (max-width: 768px) {
      ul[data-type="taskList"] li > label {
        margin-right: 0.5rem;
      }
    }

    ul[data-type="taskList"] li > label input[type="checkbox"] {
      -webkit-appearance: none;
      appearance: none;
      background-color: hsl(var(--background));
      margin: 0;
      cursor: pointer;
      width: 1.2em;
      height: 1.2em;
      position: relative;
      top: 5px;
      border: 2px solid hsl(var(--border));
      margin-right: 0.3rem;
      display: grid;
      place-content: center;

      &:hover {
        background-color: hsl(var(--accent));
      }

      &:active {
        background-color: hsl(var(--accent));
      }

      &::before {
        content: "";
        width: 0.65em;
        height: 0.65em;
        transform: scale(0);
        transition: 120ms transform ease-in-out;
        box-shadow: inset 1em 1em;
        transform-origin: center;
        clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
      }

      &:checked::before {
        transform: scale(1);
      }
    }

    ul[data-type="taskList"] li[data-checked="true"] > div > p {
      color: var(--muted-foreground);
      text-decoration: line-through;
      text-decoration-thickness: 2px;
    }

    /* Overwrite tippy-box original max-width */
    .tippy-box {
      max-width: 400px !important;
    }

    .ProseMirror:not(.dragging) .ProseMirror-selectednode {
      outline: none !important;
      background-color: var(--novel-highlight-blue);
      transition: background-color 0.2s;
      box-shadow: none;
    }

    .drag-handle {
      position: fixed;
      opacity: 1;
      transition: opacity ease-in 0.2s;
      border-radius: 0.25rem;

      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10' style='fill: rgba(0, 0, 0, 0.5)'%3E%3Cpath d='M3,2 C2.44771525,2 2,1.55228475 2,1 C2,0.44771525 2.44771525,0 3,0 C3.55228475,0 4,0.44771525 4,1 C4,1.55228475 3.55228475,2 3,2 Z M3,6 C2.44771525,6 2,5.55228475 2,5 C2,4.44771525 2.44771525,4 3,4 C3.55228475,4 4,4.44771525 4,5 C4,5.55228475 3.55228475,6 3,6 Z M3,10 C2.44771525,10 2,9.55228475 2,9 C2,8.44771525 2.44771525,8 3,8 C3.55228475,8 4,8.44771525 4,9 C4,9.55228475 3.55228475,10 3,10 Z M7,2 C6.44771525,2 6,1.55228475 6,1 C6,0.44771525 6.44771525,0 7,0 C7.55228475,0 8,0.44771525 8,1 C8,1.55228475 7.55228475,2 7,2 Z M7,6 C6.44771525,6 6,5.55228475 6,5 C6,4.44771525 6.44771525,4 7,4 C7.55228475,4 8,4.44771525 8,5 C8,5.55228475 7.55228475,6 7,6 Z M7,10 C6.44771525,10 6,9.55228475 6,9 C6,8.44771525 6.44771525,8 7,8 C7.55228475,8 8,8.44771525 8,9 C8,9.55228475 7.55228475,10 7,10 Z'%3E%3C/path%3E%3C/svg%3E");
      background-size: calc(0.5em + 0.375rem) calc(0.5em + 0.375rem);
      background-repeat: no-repeat;
      background-position: center;
      width: 1.2rem;
      height: 1.5rem;
      z-index: 50;
      cursor: grab;

      &:hover {
        background-color: var(--novel-stone-100);
        transition: background-color 0.2s;
      }

      &:active {
        background-color: var(--novel-stone-200);
        transition: background-color 0.2s;
        cursor: grabbing;
      }

      &.hide {
        opacity: 0;
        pointer-events: none;
      }

      @media screen and (max-width: 600px) {
        display: none;
        pointer-events: none;
      }
    }

    .dark .drag-handle {
      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10' style='fill: rgba(255, 255, 255, 0.5)'%3E%3Cpath d='M3,2 C2.44771525,2 2,1.55228475 2,1 C2,0.44771525 2.44771525,0 3,0 C3.55228475,0 4,0.44771525 4,1 C4,1.55228475 3.55228475,2 3,2 Z M3,6 C2.44771525,6 2,5.55228475 2,5 C2,4.44771525 2.44771525,4 3,4 C3.55228475,4 4,4.44771525 4,5 C4,5.55228475 3.55228475,6 3,6 Z M3,10 C2.44771525,10 2,9.55228475 2,9 C2,8.44771525 2.44771525,8 3,8 C3.55228475,8 4,8.44771525 4,9 C4,9.55228475 3.55228475,10 3,10 Z M7,2 C6.44771525,2 6,1.55228475 6,1 C6,0.44771525 6.44771525,0 7,0 C7.55228475,0 8,0.44771525 8,1 C8,1.55228475 7.55228475,2 7,2 Z M7,6 C6.44771525,6 6,5.55228475 6,5 C6,4.44771525 6.44771525,4 7,4 C7.55228475,4 8,4.44771525 8,5 C8,5.55228475 7.55228475,6 7,6 Z M7,10 C6.44771525,10 6,9.55228475 6,9 C6,8.44771525 6.44771525,8 7,8 C7.55228475,8 8,8.44771525 8,9 C8,9.55228475 7.55228475,10 7,10 Z'%3E%3C/path%3E%3C/svg%3E");
    }

    /* Custom Youtube Video CSS */
    iframe {
      border: 8px solid #ffd00027;
      border-radius: 4px;
      min-width: 200px;
      min-height: 200px;
      display: block;
      outline: 0px solid transparent;
    }

    div[data-youtube-video] > iframe {
      cursor: move;
      aspect-ratio: 16 / 9;
      width: 100%;
    }

    .ProseMirror-selectednode iframe {
      transition: outline 0.15s;
      outline: 6px solid #fbbf24;
    }

    @media only screen and (max-width: 480px) {
      div[data-youtube-video] > iframe {
        max-height: 50px;
      }
    }

    @media only screen and (max-width: 720px) {
      div[data-youtube-video] > iframe {
        max-height: 100px;
      }
    }

    /* CSS for bold coloring and highlighting issue*/
    span[style] > strong {
      color: inherit;
    }

    mark[style] > strong {
      color: inherit;
    }

    ```

  </Accordion>
  <Accordion title="Globals CSS" icon="globe">
    Update your globals.css with Novel css vars
    ```css globals.css
    @tailwind base;
    @tailwind components;
    @tailwind utilities;

    @layer base {
      :root {
        ...
        --novel-highlight-default: #ffffff;
        --novel-highlight-purple: #f6f3f8;
        --novel-highlight-red: #fdebeb;
        --novel-highlight-yellow: #fbf4a2;
        --novel-highlight-blue: #c1ecf9;
        --novel-highlight-green: #acf79f;
        --novel-highlight-orange: #faebdd;
        --novel-highlight-pink: #faf1f5;
        --novel-highlight-gray: #f1f1ef;

      }

      .dark {
        ....

        --novel-highlight-default: #000000;
        --novel-highlight-purple: #3f2c4b;
        --novel-highlight-red: #5c1a1a;
        --novel-highlight-yellow: #5c4b1a;
        --novel-highlight-blue: #1a3d5c;
        --novel-highlight-green: #1a5c20;
        --novel-highlight-orange: #5c3a1a;
        --novel-highlight-pink: #5c1a3a;
        --novel-highlight-gray: #3a3a3a;

      }
    }

    ```

  </Accordion>
</AccordionGroup>

<Note>You need `require("@tailwindcss/typography")` for the prose styling</Note>

## Usage within Dialogs

Novel has been designed to work automatically within Radix Dialogs, namely by looking for the closest parent attribute `[role="dialog"]`. If you're using a different implementation for popups and dialogs, ensure you add this attribute above the editor so the drag handle calculates the correct position.
